#!/usr/bin/env python
import os, sys, re, logging
from optparse import OptionParser, IndentedHelpFormatter

logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger()

class BadLineException(Exception):
    """
    This exception is raised by all subclasses of HandlerInterface
    when they are instructed to handle a line which they cannot.
    """
    pass

class HandlerInterface:
    """
    This class defines the methods that all line handlers should implement.
    """
    def handle(self, line):
        """
        This Method should perform any processing necessary to handle
        the line. If the class handles more than one line, handle will
        usually add lines to a buffer until an unprocessable line is
        encountered.

        If a line cannot be processed, handle should raise
        BadLineException.
        """
        raise NotImplementedException
    
    def __str__(self):
        """
        subclasses should override __str__ to provide a relevant
        Man-Page fragment.
        """
        raise NotImplementedException

class LineHandler(HandlerInterface):
    """
    Base class for Handlers that handle only single lines.
    """
    def __init__(self, regex, line):
        self.regex = re.compile(regex)
        self.line = line
        self.check(line)
        log.debug("%s, INIT: accepting '%s'" %(self.__class__, line))

    def check(self, line):
        if self.regex is None: return
        if not self.regex.match(line):
            raise BadLineException
        
    def handle(self, line):
        if self.line is not None:
            raise BadLineException
        self.check(line)
        self.line = line
        
    def uncomment(self, line, remove_leading_ws=True):
        if remove_leading_ws:
            return re.sub(r'^%\s*', "", line)
        else:
            return re.sub(r'^%', "", line)

    def __str__(self):
        log.error("LineHandler used directly")
        raise NotImplementedError

class BlockHandler(LineHandler):
    """
    Base class for Handlers that handle blocks of text. A block ends
    on an empty comment line.
    NOTE: The first line is placed in self.line
    """
    def __init__(self, regex, line):
        LineHandler.__init__(self, regex, line)
        self.empty = re.compile(r'^%\s*$')
        self.buf = []
        
    def is_empty(self, line):
        return self.uncomment(line, True) == ""
        
    def handle(self, line):
        if not self.is_empty(line):
            self.buf.append(self.uncomment(line))
        else:
            raise BadLineException
        
    def __str__(self):
        log.error("BlockHandler used directly")
        raise NotImplementedError
        # return "\n".join(self.buf)
            
class NullHandler(HandlerInterface):
    """
    Convenience-class that does nothing (always raises
    BadLineException when told to handle a line).
    """
    def __init__(self, line):
        raise BadLineException
    def handle(self, line):
        raise BadLineException
    def __str__(self):
        return ""

class EmptyLineHandler(LineHandler):
    def __init__(self):
        LineHandler.__init__(self, r'^\s*$')

    def __str__(self):
        return "\n"

class EmptyCommentLineHandler(LineHandler):
    def __init__(self, line):
        LineHandler.__init__(self, r'^%\s*$', line)

    def __str__(self): return "\n"

class CVSLineHandler(LineHandler):
    """
    Accepts (and ignores) CVS lines
    """
    def __init__(self):
        LineHandler.__init__(self, r'[$][A-Z][a-z]+[^$]*[$]')
    def __str__(self): return ""

class ListHandler(BlockHandler):
    def handle(self, line):
        if self.line:
            log.debug("check for list continuation")
            try: self.check(line)
            except BadLineException: pass
            else: raise BadLineException
        log.debug("regular processing")
        BlockHandler.handle(self, line)
                
class ULItemHandler(ListHandler):
    def __init__(self, line):
        BlockHandler.__init__(self, r'^%\s*[-*]\s+(.*)', line)

    def __str__(self):
        #self.buf[0] = re.sub(r'^\s*[-*]\s+', '', self.buf[0])
        self.line = self.regex.match(self.line).group(1)
        return ".IP \\(bu\n" + self.line + "".join(self.buf)

class ParagraphHandler(BlockHandler):
    def __init__(self, line):
        BlockHandler.__init__(self, r'[^\s]', line)
    def __str__(self):
        return ".PP\n" + self.uncomment(self.line) + "".join(self.buf)

class CodeHandler(BlockHandler):
    """Handles blocks of preformatted text.

    Blocks of pre-formatted text should be surrounded by html-like
    tags. The tags must be on a line of their own. Oh, and don't
    forget the end-tags!
    
    Example:
    <pre>
    this {
      is; some();
      example(code);
    }
    </pre>
    """
    def __init__(self, line):
        self.end_code = re.compile(r'%\s*</pre>')
        self.done = False
        BlockHandler.__init__(self, r'%\s*<pre>', line)

    def handle(self, line):
        if self.done:
            log.debug("found code end-tag")
            raise BadLineException
        elif self.end_code.match(line):
            # swallow line containing end tag. Next line will end
            # processing.
            self.done = True
        else:
            # remove only the starting percent sign
            self.buf.append(line[1:])

    def __str__(self):
        return ".nf\n" + "".join(self.buf) + ".fi\n"
    
class DLItemHandler(ListHandler):
    def __init__(self, line):
        BlockHandler.__init__(self, r'^%\s*([\w_ ]+):\s\s+(.*)', line)

    def __str__(self):
        dt, rest = self.regex.match(self.line).groups()
        return (".TP\n%s\n%s\n" % (dt, rest)) + "".join(self.buf)

class HeadingHandler(LineHandler):
    def __init__(self, line):
        LineHandler.__init__(self, r'^%([A-Z\s0-9]+)$', line)
    def __str__(self):
        return ".SH %s" % self.uncomment(self.line)

class FirstLineHandler(HandlerInterface):
    def __init__(self, line="", name="NAME", section="SECTION",
                 date="DATE", manual="MANUAL", desc="DESC"):
        self.name = name
        self.line = line
        self.section = section
        self.date = date
        self.manual = manual
        self.desc = desc
        self.function_line = ""
        self.first_comment_line = ""

    def handle(self, line):
        if line.startswith("function"):
            self.function_line = line
        elif self.first_comment_line:
            raise BadLineException
        else:
            # this is the first comment line
            self.first_comment_line = line
            try:
                name, desc = re.split(r'\s+-?\s*', line, 1)
            except ValueError:
                log.warn("first non-comment line doesn't contain "
                         "function name and description")
                raise BadLineException
            else:
                name = re.sub("^%\s*", "", name)
                self.name = name
                self.desc = desc
        
    def __str__(self):
        res='.TH "%(name)s" "%(section)s" "%(date)s" "%(name)s" "%(manual)s"\n'\
            '.SH NAME\n' \
            '%(name)s \- %(desc)s\n' % vars(self)
        if self.function_line:
            res += ".SH SYNOPSIS\n.B %s\n" % self.function_line
        res += '.SH DESCRIPTION\n'
        return res


class HandlerFactory:
    def __init__(self):
        self.default_handler = FirstLineHandler
        self.handlers = [
            EmptyCommentLineHandler,
            ULItemHandler,
            DLItemHandler,
            HeadingHandler,
            CodeHandler,
            ParagraphHandler,
            ]

    def set_default_handler(self, handler_class):
        self.default_handler = handler_class

    def add_handler(self, handler_class, prepend=False):
        if prepend:
            self.handlers.insert(0, handler_class)
        else:
            self.handlers.append(handler_class)

    def get_handler(self, line=None):
        if line is None:
            log.debug("returning default handler: %s",
                      repr(self.default_handler))
            return self.default_handler()
        for handler in self.handlers:
            try:
                log.debug("trying %s", handler.__name__)
                h = handler(line)
            except BadLineException:
                continue
            else:
                return h

class Main:
    """
mat2man generates a man page out of the documentation embedded in a
Matlab .m file. It should be formatted as follows:

function [outarg1, outarg2] = fubar(inarg1, inarg2);
%fubar - frobs all ralls.
% Long description of just how fubar frobs the ralls. Lorem
% ipsum dolor sit amet etc.
%
% OPTIONS
% outarg1:      first result of frobbing.
% outarg2:      more information
% please note:  there must be >= two
%               spaces after the colon.
%
% EXAMPLE
%<pre>
%this {
%    is(some);
%    example.code();
%}
%</pre>
%
% MORE LOREM IPSUM
% - Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
% - Nunc semper sapien nec odio. Integer arcu mi, facilisis
%   eget, porta sed, semper at, tortor.
% - Pellentesque consequat purus facilisis dolor.

If present, the function definition is used as the synopsis. Blank
lines are ignored, bulleted lists may be created by using '-' or '*'
as list item delimiters. %prog uses blank comment lines as
paragraph/list-item seperators.
"""

    def __init__(self):
        self.version = "0.1"
        self.epilog = """
If -o is not specified, output is sent to standard output.

Examples:
  %prog foo.m
      parses foo.m and writes to standard output.

  %prog --section=1 --outfile=foo.1
      reads from stdin and writes foo.1

  cat fubar.m | %prog | nroff -man | less
      displays documentation contained in fubar.m

  %prog --help | egrep '^(function|%)' | %prog | nroff -man | less
      view the example Matlab documentation above as man page :-)
"""
        self.input = sys.stdin
        self.output = sys.stdout

    def handle_args(self):
        class MyHelpFormatter(IndentedHelpFormatter):
            def format_description(self, description):
                if not description: description = ""
                return description
    
            def format_epilog(self, epilog):
                if not epilog: epilog = ""
                return epilog

        def get_date():
            import locale, time
            loc = locale.getlocale(locale.LC_ALL)
            locale.setlocale(locale.LC_ALL, 'C')
            d = time.strftime("%B %Y", time.localtime())
            locale.setlocale(locale.LC_ALL, loc)
            return d

        parser = OptionParser(
            version = self.version,
            description = self.__doc__,
            usage = "Usage: %prog [-s section] [-o outfile] [infile.m]",
            formatter = MyHelpFormatter(),
            epilog = self.epilog)
        parser.add_option(
            "-o", "--output",
            help="write output to 'FILE'",
            type="string", dest="outfile", metavar="FILE")
        parser.add_option(
            "-s", "--section",
            help="section number for man page (default: %default)",
            type="string", dest="section")
        parser.add_option(
            "-m", "--manual",
            help="name of manual (default: %default)",
            type="string", dest="manual")
        parser.add_option(
            "-d", "--date",
            type="string", dest="date")
        parser.add_option(
            "-v", "--verbose",
            help="be more verbose",
            action="count", dest="verbose")
        parser.add_option(
            "-D", "--debug",
            help="print debugging information",
            action="store_true", default=True, dest="debug")
        parser.set_defaults(
            outfile="-",
            section="3mat",
            manual="Matlab Documentation",
            date=get_date())
        opts, args = parser.parse_args()
        if len(args)>1:
            parser.error("cannot handle more than one input file")

        if opts.outfile != "-":
            try: self.output = open(opts.outfile, "w")
            except err:
                parser.error(err)

        if len(args) > 0:
            try: self.input = open(args[0])
            except err: parser.error(err)
        if opts.debug: log.setLevel(logging.DEBUG)
        self.manual = opts.manual
        self.section = opts.section
        self.date = opts.date
        
        
    def comment_lines(self, file=None):
        if file is None: file = self.input
        blank_line = re.compile(r"^\s*$")
        #self.cvs_tags = re.compile(r"\$[[:upper:]][[:lower:]]+[^\$]*\$")
        for line in file:
            # skip blank lines
            if blank_line.match(line): continue
            # processing ends with first non-blank, non-comment line
            if not (line.startswith("%") or
                    line.startswith("function")): return
            # ignore CVS commands
            # if cvs_tag.match(line): continue
            log.debug(line)
            yield line

    def out(self, what):
        res = str(what)
        if not res.endswith("\n"): res += "\n"
        self.output.write(res)

    def main_loop(self):
        """
        Main processing loop
        """
        factory = HandlerFactory()
        handler = factory.get_handler()
        handler.section = self.section
        handler.manual = self.manual
        handler.date = self.date
        log.debug("Initial Logger: %s", repr(handler))
        for line in self.comment_lines(self.input):
            try:
                handler.handle(line)
            except BadLineException:
                res = str
                self.out(handler)
                handler = factory.get_handler(line)
                log.debug("New Logger: %s", repr(handler))
        self.out(handler)

    def main(self):
        self.handle_args()
        self.main_loop()

if __name__=="__main__":
    Main().main()
